/*global document window*/
import type GraphContainer from '@gitkraken/gitkraken-components';
import type { CssVariables, GraphRow, GraphZoneType } from '@gitkraken/gitkraken-components';
import { consume } from '@lit/context';
import { SignalWatcher } from '@lit-labs/signals';
import { html, LitElement } from 'lit';
import { customElement, query, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import type { GitGraphRowType } from '../../../../../git/models/graph';
import { filterMap } from '../../../../../system/array';
import { getCssMixedColorValue, getCssOpacityColorValue, getCssVariable } from '../../../../../system/color';
import { debounce } from '../../../../../system/function/debounce';
import {
	DoubleClickedCommandType,
	GetMissingAvatarsCommand,
	GetMissingRefsMetadataCommand,
	GetMoreRowsCommand,
	UpdateColumnsCommand,
	UpdateRefsVisibilityCommand,
	UpdateSelectionCommand,
} from '../../../../plus/graph/protocol';
import type { CustomEventType } from '../../../shared/components/element';
import { ipcContext } from '../../../shared/contexts/ipc';
import type { TelemetryContext } from '../../../shared/contexts/telemetry';
import { telemetryContext } from '../../../shared/contexts/telemetry';
import type { Disposable } from '../../../shared/events';
import type { ThemeChangeEvent } from '../../../shared/theme';
import { onDidChangeTheme } from '../../../shared/theme';
import { graphStateContext } from '../stateProvider';
import type { GlGraph } from './gl-graph';
import type { GraphWrapperTheming } from './gl-graph.react';
import './gl-graph';

// These properties in the DOM are auto-generated by VS Code from our `contributes.colors` in package.json
const graphLaneThemeColors = new Map([
	['--vscode-gitlens-graphLane1Color', '#15a0bf'],
	['--vscode-gitlens-graphLane2Color', '#0669f7'],
	['--vscode-gitlens-graphLane3Color', '#8e00c2'],
	['--vscode-gitlens-graphLane4Color', '#c517b6'],
	['--vscode-gitlens-graphLane5Color', '#d90171'],
	['--vscode-gitlens-graphLane6Color', '#cd0101'],
	['--vscode-gitlens-graphLane7Color', '#f25d2e'],
	['--vscode-gitlens-graphLane8Color', '#f2ca33'],
	['--vscode-gitlens-graphLane9Color', '#7bd938'],
	['--vscode-gitlens-graphLane10Color', '#2ece9d'],
]);

declare global {
	// interface HTMLElementTagNameMap {
	// 	'gl-graph-wrapper': GlGraphWrapper;
	// }

	interface GlobalEventHandlersEventMap {
		// passing up event map
		'gl-graph-change-selection': CustomEvent<{ selection: GraphRow[] }>;
		'gl-graph-change-visible-days': CustomEvent<{ top: number; bottom: number }>;
		'gl-graph-mouse-leave': CustomEvent<void>;
		'gl-graph-row-context-menu': CustomEvent<{ graphZoneType: GraphZoneType; graphRow: GraphRow }>;
		'gl-graph-row-hover': CustomEvent<{
			graphZoneType: GraphZoneType;
			graphRow: GraphRow;
			clientX: number;
			currentTarget: HTMLElement;
		}>;
		'gl-graph-row-unhover': CustomEvent<{
			graphZoneType: GraphZoneType;
			graphRow: GraphRow;
			relatedTarget: EventTarget | null;
		}>;
	}
}

@customElement('gl-graph-wrapper')
export class GlGraphWrapper extends SignalWatcher(LitElement) {
	// use Light DOM
	protected override createRenderRoot(): HTMLElement | DocumentFragment {
		return this;
	}

	private disposables: Disposable[] = [];

	@consume({ context: graphStateContext, subscribe: true })
	private readonly graphState!: typeof graphStateContext.__context__;

	@consume({ context: ipcContext })
	private readonly _ipc!: typeof ipcContext.__context__;

	@consume({ context: telemetryContext as any })
	private readonly _telemetry!: TelemetryContext;

	@query('gl-graph')
	graph!: typeof GlGraph;

	private ref?: GraphContainer;
	private onSetRef = (ref: GraphContainer) => {
		this.ref = ref;
	};

	@state()
	private theming?: GraphWrapperTheming;

	override connectedCallback(): void {
		super.connectedCallback?.();

		this.theming = this.getGraphTheming();
		this.disposables.push(
			onDidChangeTheme(
				debounce((e: ThemeChangeEvent) => {
					this.theming = this.getGraphTheming(e);
				}, 100),
			),
		);
	}

	override disconnectedCallback(): void {
		super.disconnectedCallback?.();

		this.disposables.forEach(d => d.dispose());
		this.disposables = [];
	}

	override render() {
		const { graphState } = this;

		return html`<gl-graph
			.setRef=${this.onSetRef}
			.activeRow=${graphState.activeRow}
			.avatars=${graphState.avatars}
			.columns=${graphState.columns}
			.config=${graphState.config}
			.context=${graphState.context}
			.downstreams=${graphState.downstreams}
			.excludeRefs=${graphState.excludeRefs}
			.excludeTypes=${graphState.excludeTypes}
			.filter=${graphState.filter}
			.includeOnlyRefs=${graphState.includeOnlyRefs}
			?loading=${graphState.loading}
			nonce=${ifDefined(graphState.nonce)}
			.paging=${graphState.paging}
			.refsMetadata=${graphState.refsMetadata}
			.rows=${graphState.rows}
			.rowsStats=${graphState.rowsStats}
			.searchResults=${graphState.searchResults}
			.selectedRows=${graphState.selectedRows}
			.theming=${this.theming}
			?windowFocused=${graphState.windowFocused}
			.workingTreeStats=${graphState.workingTreeStats}
			@changecolumns=${this.onColumnsChanged}
			@changerefsvisibility=${this.onRefsVisibilityChanged}
			@changeselection=${this.onSelectionChanged}
			@changevisibledays=${this.onVisibleDaysChanged}
			@missingavatars=${this.onMissingAvatars}
			@missingrefsmetadata=${this.onMissingRefsMetadata}
			@morerows=${this.onGetMoreRows}
			@graphmouseleave=${this.onMouseLeave}
			@refdoubleclick=${this.onRefDoubleClick}
			@rowcontextmenu=${this.onRowContextMenu}
			@rowdoubleclick=${this.onRowDoubleClick}
			@rowhover=${this.onRowHover}
			@rowunhover=${this.onRowUnhover}
		></gl-graph>`;
	}

	selectCommits(shaList: string[], includeToPrevSel: boolean, isAutoOrKeyScroll: boolean) {
		this.ref?.selectCommits(shaList, includeToPrevSel, isAutoOrKeyScroll);
	}

	private onColumnsChanged(event: CustomEventType<'graph-changecolumns'>) {
		this._ipc.sendCommand(UpdateColumnsCommand, { config: event.detail.settings });
	}

	private onGetMoreRows({ detail: sha }: CustomEventType<'graph-morerows'>) {
		this.graphState.loading = true;
		this._ipc.sendCommand(GetMoreRowsCommand, { id: sha });
	}

	private onMouseLeave() {
		this.dispatchEvent(new CustomEvent('gl-graph-mouse-leave'));
	}

	private onMissingAvatars({ detail: emails }: CustomEventType<'graph-missingavatars'>) {
		this._ipc.sendCommand(GetMissingAvatarsCommand, { emails: emails });
	}

	private onMissingRefsMetadata({ detail: metadata }: CustomEventType<'graph-missingrefsmetadata'>) {
		this._ipc.sendCommand(GetMissingRefsMetadataCommand, { metadata: metadata });
	}

	private onRefDoubleClick({ detail: { ref, metadata } }: CustomEventType<'graph-doubleclickref'>) {
		this._ipc.sendCommand(DoubleClickedCommandType, { type: 'ref', ref: ref, metadata: metadata });
	}

	private onRefsVisibilityChanged({ detail }: CustomEventType<'graph-changerefsvisibility'>) {
		this._ipc.sendCommand(UpdateRefsVisibilityCommand, detail);
	}

	private onRowContextMenu({ detail: { graphRow, graphZoneType } }: CustomEventType<'graph-rowcontextmenu'>) {
		this.dispatchEvent(
			new CustomEvent('gl-graph-row-context-menu', {
				detail: { graphZoneType: graphZoneType, graphRow: graphRow },
			}),
		);
	}

	private onRowDoubleClick({ detail: { row, preserveFocus } }: CustomEventType<'graph-doubleclickrow'>) {
		this._ipc.sendCommand(DoubleClickedCommandType, {
			type: 'row',
			row: { id: row.sha, type: row.type as GitGraphRowType },
			preserveFocus: preserveFocus,
		});
	}

	private onRowHover({ detail }: CustomEventType<'graph-graphrowhovered'>) {
		this.dispatchEvent(new CustomEvent('gl-graph-row-hover', { detail: detail }));
	}

	private onRowUnhover({ detail }: CustomEventType<'graph-graphrowunhovered'>) {
		this.dispatchEvent(new CustomEvent('gl-graph-row-unhover', { detail: detail }));
	}

	private onSelectionChanged({ detail: rows }: CustomEventType<'graph-changeselection'>) {
		const selection = filterMap(rows, r =>
			r != null ? { id: r.sha, type: r.type as GitGraphRowType } : undefined,
		);

		const active = rows[rows.length - 1];
		const activeKey = active != null ? `${active.sha}|${active.date}` : undefined;
		this.graphState.activeRow = activeKey;
		this.graphState.activeDay = active?.date;

		this.dispatchEvent(new CustomEvent('gl-graph-change-selection', { detail: { selection: selection } }));
		this._ipc.sendCommand(UpdateSelectionCommand, { selection: selection });
	}

	private onVisibleDaysChanged({ detail }: CustomEventType<'graph-changevisibledays'>) {
		this.dispatchEvent(new CustomEvent('gl-graph-change-visible-days', { detail: detail }));
	}

	private readonly themingDefaults: { cssVariables: CssVariables; themeOpacityFactor: number } = {
		cssVariables: (() => {
			const bgColor = getCssVariableValue('--color-background');
			const mixedGraphColors: CssVariables = {};
			let i = 0;
			let color;
			for (const [colorVar, colorDefault] of graphLaneThemeColors) {
				color = getCssVariableValue(colorVar, { fallbackValue: colorDefault });
				mixedGraphColors[`--graph-color-${i}`] = color;
				for (const mixInt of [15, 25, 45, 50]) {
					mixedGraphColors[`--graph-color-${i}-bg${mixInt}`] = getCssMixedColorValue(bgColor, color, mixInt);
				}
				for (const mixInt of [10, 50]) {
					mixedGraphColors[`--graph-color-${i}-f${mixInt}`] = getCssOpacityColorValue(color, mixInt);
				}
				i++;
			}
			return {
				'--app__bg0': getCssVariableValue('--color-background'),
				'--panel__bg0': getCssVariableValue('--color-graph-background'),
				'--panel__bg1': getCssVariableValue('--color-graph-background2'),
				'--section-border': getCssVariableValue('--color-graph-background2'),
				'--selected-row': getCssVariableValue('--color-graph-selected-row'),
				'--selected-row-border': 'none',
				'--hover-row': getCssVariableValue('--color-graph-hover-row'),
				'--hover-row-border': 'none',
				'--scrollable-scrollbar-thickness': getCssVariableValue('--graph-column-scrollbar-thickness'),
				'--scroll-thumb-bg': getCssVariableValue('--vscode-scrollbarSlider-background'),
				'--scroll-marker-head-color': getCssVariableValue('--color-graph-scroll-marker-head'),
				'--scroll-marker-upstream-color': getCssVariableValue('--color-graph-scroll-marker-upstream'),
				'--scroll-marker-highlights-color': getCssVariableValue('--color-graph-scroll-marker-highlights'),
				'--scroll-marker-local-branches-color': getCssVariableValue(
					'--color-graph-scroll-marker-local-branches',
				),
				'--scroll-marker-remote-branches-color': getCssVariableValue(
					'--color-graph-scroll-marker-remote-branches',
				),
				'--scroll-marker-stashes-color': getCssVariableValue('--color-graph-scroll-marker-stashes'),
				'--scroll-marker-tags-color': getCssVariableValue('--color-graph-scroll-marker-tags'),
				'--scroll-marker-selection-color': getCssVariableValue('--color-graph-scroll-marker-selection'),
				'--scroll-marker-pull-requests-color': getCssVariableValue('--color-graph-scroll-marker-pull-requests'),
				'--stats-added-color': getCssVariableValue('--color-graph-stats-added'),
				'--stats-deleted-color': getCssVariableValue('--color-graph-stats-deleted'),
				'--stats-files-color': getCssVariableValue('--color-graph-stats-files'),
				'--stats-bar-border-radius': getCssVariableValue('--graph-stats-bar-border-radius'),
				'--stats-bar-height': getCssVariableValue('--graph-stats-bar-height'),
				'--text-selected': getCssVariableValue('--color-graph-text-selected'),
				'--text-selected-row': getCssVariableValue('--color-graph-text-selected-row'),
				'--text-hovered': getCssVariableValue('--color-graph-text-hovered'),
				'--text-dimmed-selected': getCssVariableValue('--color-graph-text-dimmed-selected'),
				'--text-dimmed': getCssVariableValue('--color-graph-text-dimmed'),
				'--text-normal': getCssVariableValue('--color-graph-text-normal'),
				'--text-secondary': getCssVariableValue('--color-graph-text-secondary'),
				'--text-disabled': getCssVariableValue('--color-graph-text-disabled'),
				'--text-accent': getCssVariableValue('--color-link-foreground'),
				'--text-inverse': getCssVariableValue('--vscode-input-background'),
				'--text-bright': getCssVariableValue('--vscode-input-background'),
				...mixedGraphColors,
			};
		})(),
		themeOpacityFactor: 1,
	};

	private getGraphTheming(e?: ThemeChangeEvent): GraphWrapperTheming {
		// this will be called on theme updated as well as on config updated since it is dependent on the column colors from config changes and the background color from the theme
		const computedStyle = e?.computedStyle ?? window.getComputedStyle(document.documentElement);
		const bgColor = getCssVariableValue('--color-background', { computedStyle: computedStyle });

		const mixedGraphColors: CssVariables = {};

		let i = 0;
		let color;
		for (const [colorVar, colorDefault] of graphLaneThemeColors) {
			color = getCssVariableValue(colorVar, { computedStyle: computedStyle, fallbackValue: colorDefault });

			mixedGraphColors[`--column-${i}-color`] = getCssVariable(colorVar, computedStyle) || colorDefault;

			for (const mixInt of [15, 25, 45, 50]) {
				mixedGraphColors[`--graph-color-${i}-bg${mixInt}`] = getCssMixedColorValue(bgColor, color, mixInt);
			}

			i++;
		}

		const isHighContrastTheme =
			e?.isHighContrastTheme ??
			(document.body.classList.contains('vscode-high-contrast') ||
				document.body.classList.contains('vscode-high-contrast-light'));

		return {
			cssVariables: {
				...this.themingDefaults.cssVariables,
				'--selected-row-border': isHighContrastTheme
					? `1px solid ${getCssVariableValue('--color-graph-contrast-border', { computedStyle: computedStyle })}`
					: 'none',
				'--hover-row-border': isHighContrastTheme
					? `1px dashed ${getCssVariableValue('--color-graph-contrast-border', { computedStyle: computedStyle })}`
					: 'none',
				...mixedGraphColors,
			},
			themeOpacityFactor:
				parseInt(getCssVariable('--graph-theme-opacity-factor', computedStyle)) ||
				this.themingDefaults.themeOpacityFactor,
		};
	}
}

function getCssVariableValue(
	variable: string,
	options?: { computedStyle?: CSSStyleDeclaration; fallbackValue?: string },
): string {
	const fallbackValue = options?.computedStyle
		? getCssVariable(variable, options?.computedStyle)
		: options?.fallbackValue
			? options.fallbackValue
			: undefined;

	if (fallbackValue) {
		return `var(${variable}, ${fallbackValue})`;
	}
	return `var(${variable})`;
}
